require 'xml/libxml'

module KindDom

  # KindDom provides graceful access to the the DOM of an XML document using
  # three methods of kindness: 
  # - <tt>#collection_of</tt> to select a collection of nodes
  # - <tt>#first_of</tt> to select one node
  # - <tt>#content_for</tt> to get node content.
  #
  # The original libxml behavior of the XML document is preserved through the #dom accessor.
  #
  # As a contrived example, in the controller:
  #    @results = KindDom::Base.new(xml_data)
  # 
  # In the view:
  #    
  #    <% @results.first_of('//item') do |item| -%>
  #      <p>(This block is only executed if `item` is found.)</p>
  #    
  #      <% item.content_for('title', '(Untitled)') do |content| -%>
  #        <h2><%=h content %></h2>
  #        <p>(This header will show "(Untitled)" if `title` is blank or not found.)</p>
  #      <% end -%>
  #     
  #      <% item.content_for('description') do |content| -%>
  #        <p><%= item.content_for('@updated_at') {|date| "<em>#{date}</em> -- " } %>
  #          <%=h content %></p>
  #        <p>(This block is only executed if `description` has content.)</p>
  #      <% end -%>
  #    <% end -%>
  #
  class Base
  
    attr_reader :dom
    attr_accessor :find_cache
  
    # A new KindDom object may be created from raw XML [string] data, or 
    # an already instantiated KindDom::Base, XML::Node or XML::Document.
    #
    # Caches intermediate found values, before yielding to the optional block. 
    # Disable the cache with opt `:find_cache => false` or set #find_cache to false
    def initialize(xml_in=nil, opts={})
      unless xml_in.nil?
        case 
        when xml_in.kind_of?(KindDom::Base)
          @dom = xml_in.dom
        when xml_in.kind_of?(XML::Document), xml_in.kind_of?(XML::Node)
          @dom = xml_in
        when xml_in.kind_of?(String)
          parser = XML::Parser.new
          parser.string = xml_in
          @dom = parser.parse
        end
      end
      @find_cache = {} if opts[:find_cache].nil? ? true : opts[:find_cache]
    rescue XML::Parser::ParseError
    end
  
    # Retrieve the contents of the first node or attribute selected by the XPath; 
    # defaults to the current node, "."
    #
    # Optional second argument is the default value to return if the DOM find fails.
    #
    # When a block is provided, it will be called with the found content 
    # (or default) before returning.
    #
    def content_for(xpath='.', default=nil)       # :yields: found_content
      content = cache "content_for(#{xpath})" do
        node = case @dom.class.to_s
        when 'XML::Document' then
          @dom.root.find_first(xpath)
        else # 'XML::Node'
          @dom.find_first(xpath)
        end
        case node.class.to_s
        when 'XML::Attr' then
          node.value
        else
          node.content
        end
      end
    rescue NoMethodError
    ensure
      content = content.blank? ? default : content
      if block_given? and !content.blank?
        return yield(content)
      else
        return content
      end
    end
  
    # Retrieve a collection (Array) of DOM nodes selected by the XPath.
    #
    # Each node is returned as KindDom to support #content_for, #collection_of & #first_of.
    #
    # When a block is provided, it will be called with the found collection 
    # (or default) before returning.
    #
    def collection_of(xpath, default=nil)      # :yields: found_collection
      c = cache "collection_of(#{xpath})" do
        @dom.find(xpath).collect {|n| self.class.new(n) }
      end
    rescue NoMethodError
    ensure
      c = c.blank?||c.size<1 ? default : c
      if block_given? and !c.nil?
        return yield(c)
      else
        return c
      end
    end
  
    # Retrieve the first DOM node selected by the XPath.
    #
    # The node is returned as KindDom to support #content_for, #collection_of & #first_of.
    #
    # When a block is provided, it will be called with the found node 
    # (or default) before returning.
    #
    def first_of(xpath, default=nil)      # :yields: found_node
      n = cache "first_of(#{xpath})" do
        case @dom.class.to_s
        when 'XML::Document' then
          @dom.root.find_first(xpath)
        else # 'XML::Node'
          @dom.find_first(xpath)
        end
      end
    rescue NoMethodError
    ensure
      n = n.blank? ? default : self.class.new(n)
      if block_given? and !n.blank?
        return yield(n)
      else
        return n
      end
    end
    
    private
    
    def cache(key)
      if @find_cache and @find_cache.has_key?(key)
        x = @find_cache[key]
      else
        x = yield
        @find_cache[key] = x if @find_cache
      end
      return x
    end
  end
end
